package Listening;

import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
import static java.nio.file.StandardWatchEventKind.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKind.ENTRY_DELETE;
import static java.nio.file.StandardWatchEventKind.ENTRY_MODIFY;

import java.io.File;
import java.io.IOError;
import java.io.IOException;
import java.nio.file.FileRef;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.Attributes;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;

import Commnads.ListenCommands;
import DB.DirDB;

/**
 * The Listener job is to listen to the OS, and to notify the {@link Observer}
 * about the changes.
 * We hold here {@link HashMap}s , that storing all the listened directories.
 * @author DROR
 */

public class Listener extends Thread 
{
	private Observer observer;
	private WatchService watcher;
	private ConcurrentHashMap<Path, WatchKey> keysByWatchkey ;
	private ConcurrentHashMap<WatchKey,Path> keysByPath ;
	private boolean recursive= false;
	private boolean listeningFlag;
	private boolean trace = false;


	/**
	 * Default Constructor
	 */
	public Listener()
	{
		try {
			watcher = FileSystems.getDefault().newWatchService();
		} 
		catch (IOException e) {}
		keysByWatchkey = new ConcurrentHashMap<Path,WatchKey>();
		keysByPath = new ConcurrentHashMap<WatchKey,Path> ();
		listeningFlag=false;
	}

	/**
	 * Constructor
	 * @param listencommands reference of class {@link ListenCommands} 
	 * @throws IOException
	 */
	public Listener(ListenCommands listencommands)throws IOException
	{
		observer = new Observer(listencommands);
		watcher = FileSystems.getDefault().newWatchService();
		keysByWatchkey = new ConcurrentHashMap<Path,WatchKey>();
		keysByPath = new ConcurrentHashMap<WatchKey,Path> ();
		listeningFlag=false;
	}

	/**
	 *  This function checking if listening thread had begin
	 * @return {@link Boolean} answer
	 */
	public boolean isListeningFlag() {
		return listeningFlag;
	}

	/**
	 * This function returns the {@link HashMap}
	 * @return {@link ConcurrentHashMap}
	 */
	public ConcurrentHashMap<Path, WatchKey> getKeysByWatchkey() 
	{
		return keysByWatchkey;
	}


	/**
	 * This function initialize the {@link HashMap}s with the listened directories 
	 */
	public void initDirsHash()
	{
		if (getDirectories() != null)
		{
			ArrayList<String> dirs = new ArrayList<String>(); 
			dirs.addAll(getDirectories());
			int i=0;
			int len=dirs.size();
			for (i=0;i<len;i++)
			{
				Path p = Paths.get(dirs.get(i));
				try {
					File f = new File(p.toString());
					if (f.isDirectory())
						registerAll(p);
				} 
				catch (IOException e) {e.printStackTrace();}
				catch (NullPointerException e) {}
			}
		}
	}

	/**
	 * This function initiating the listening, and notify the {@link Observer} about changes.
	 * Before listening go begin, we updating the {@link HashMap}s about the listened directories.
	 */
	public void run() 
	{
		initDirsHash();
		watching();
	}

	/**
	 * This function removing all the directories in the {@link HashMap}s
	 */
	public void removeAllDirs()
	{
		Iterator<WatchKey> iter=keysByPath.keySet().iterator();	 
		while (iter.hasNext()) 
		{
			Path path = keysByPath.get(iter.next());
			removeDir(path);
		}

	}

	/**
	 * This function returns all listened directories from {@link DirDB}
	 * @return an {@link ArrayList} of all listened directories from {@link DirDB}
	 */
	public ArrayList<String> getDirectories()
	{
		return observer.getDirectories();
	}

	/**
	 * This function remove directory from listening (from {@link HashMap}s)
	 * @param path The {@link Path} of directory
	 */
	public void removeDir(Path path)
	{
		try{
			Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
				public FileVisitResult preVisitDirectory(Path dir) {
					WatchKey key = keysByWatchkey.get(dir);
					if (key!=null)
						keysByWatchkey.get(dir).cancel();
					keysByWatchkey.remove(dir);
					keysByPath.remove(key);

					return FileVisitResult.CONTINUE;
				}
			});
		}
		catch (NullPointerException e) {}
	}

	/**
	 * This function adding a new directory to listening (to {@link HashMap}s)
	 * @param dir The {@link Path} of the directory
	 * @throws IOException
	 */
	public void addDir(Path dir) throws IOException
	{
		registerAll(dir);
	}

	/**
	 * This function stopping the listening
	 */
	public void stopListening() 
	{
		listeningFlag= false;
	}

	/**
	 * This function starting the listening
	 */
	public void startListening() 
	{
		listeningFlag= true;
	}

	@SuppressWarnings("unchecked")
	static <T> WatchEvent<T> cast(WatchEvent<?> event) {
		return (WatchEvent<T>)event;
	}

	/**
	 * This function's responsibility is to listen to OS
	 */
	public void watching() 
	{
		for (;;) 
		{
			if (listeningFlag)
			{
				WatchKey key = null;
				try {
					key = watcher.take();
				} 
				catch (InterruptedException x) 
				{
					return;
				}
				Path dir = keysByPath.get(key);
				if (dir == null) 
				{
					System.err.println("WatchKey not recognized!!");
					continue;
				}
				if (listeningFlag)
				{
					for (WatchEvent<?> event: key.pollEvents()) 
					{
						/* getting the type of event */
						WatchEvent.Kind<?> kind = event.kind();
						/*Context for directory entry event is the file name of entry*/
						WatchEvent<Path> ev = cast(event);
						Path name = ev.context();
						Path child = dir.resolve(name);

						if (listeningFlag)
						{
							/* telling to the observer about the event */
							observer.handle(event,child);

						}

						/* when directory is created, and watching recursively, then 
						 * register it and its sub-directories */
						if (recursive) {
							if (kind == ENTRY_CREATE) {
								try {
									if (Attributes.readBasicFileAttributes(child, NOFOLLOW_LINKS).isDirectory()) {
										registerAll(child);
									}
								} 
								catch (IOException x) {

								}
							}
						}
					}
				}

				/* reset key and remove from set if directory no longer accessible */
				boolean valid = key.reset();
				if (!valid) 
				{

					keysByWatchkey.remove(dir);
					keysByPath.remove(key);

					/* all directories are inaccessible */
					if (keysByPath.isEmpty() || keysByWatchkey.isEmpty()) 
					{
						break;
					}
				}
			} /* of listening condition ( listeningFlag= true) */
		} /* of the infinite loop */
	}

	/**
	 * This function register directory and its sub-directories to listening
	 * @param start The {@link Path} of the directory
	 * @throws IOException
	 */
	public void registerAll(final Path start) throws IOException {
		// register directory and sub-directories
		Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
			public FileVisitResult preVisitDirectory(Path dir) {
				try {
					register(dir);
				} catch (IOException x) {
					throw new IOError(x);
				}
				return FileVisitResult.CONTINUE;
			}
		});
	}

	/**
	 * This function register directory to listening
	 * @param dir  The {@link Path} of the directory
	 * @throws IOException
	 */
	public void register(Path dir) throws IOException {
		WatchKey key = dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
		if (trace) {
			FileRef prev = keysByPath.get(key);
			if (prev == null) {
				System.out.format("register: %s\n", dir);
			} else {
				if (!dir.equals(prev)) {
					System.out.format("update: %s -> %s\n", prev, dir);
				}
			}
		}
		keysByPath.put(key, dir);
		keysByWatchkey.put(dir, key);
	}

	/**
	 * This function updates the Log about new operation (via the {@link Observer})
	 * @param oper The operation by {@link String} that had been recognized
	 */
	public void insertToLog(String oper)
	{
		observer.insertToLog(oper);
	}

}
